function Vec2(x, y) {
    this.x = x;
    this.y = y;
}

Vec2.prototype.add = function(b) {
    return new Vec2(this.x + b.x, this.y + b.y);
};

Vec2.prototype.neg = function() {
    return this.mul(-1);
};

Vec2.prototype.mul = function(k) {
    return new Vec2(this.x * k, this.y * k);
};

Vec2.prototype.clone = function() {
    return this.mul(1);
};

Vec2.prototype.transform = function(T) {
    return T.applyTo(this);
};

Vec2.prototype.destruct = function() {
    return [this.x, this.y];
}

Vec2.prototype.destruct2 = function() {
    return {
        x: this.x,
        y: this.y,
    };
}

function Matrix2D(ihat, jhat) {
    ihat = ihat || Vec2(0, 0);
    jhat = jhat || Vec2(0, 0);

    this.i = ihat.clone();
    this.j = jhat.clone();
}

Matrix2D.prototype.mul = function(k) {
    return new Matrix2D(this.i.mul(k), this.j.mul(k));
}

Matrix2D.prototype.add = function(b) {
    return new Matrix2D(this.i.add(b.i), this.j.add(b.j));
}

Matrix2D.prototype.neg = function() {
    return this.mul(-1);
}

Matrix2D.prototype.clone = function() {
    return this.mul(1);
}

Matrix2D.prototype.applyTo = function(v) {
    return this.i.mul(v.x).add(this.j.mul(v.y));
}

function make_rotate_transform(a) {
    let c = Math.cos(-a), s = Math.sin(-a);
    return new Matrix2D(new Vec2(c, -s), new Vec2(s, c));
}

function Vec2_cast({x, y}) {
    return new Vec2(x, y);
}

function Vec2_cast2([x, y]) {
    return new Vec2(x, y);
}

const scale = 15; // 1 metre = scale px
const bwidth = 1400;
const bheight = 800;
const gravity = new Vec2(0, 9.81);
let world;
let Rapier;
let playerA;
let ctx;
let lands = [];
let land_handles = [];
let yMap;

function draw_rect(ctx, angle, position, szx, szy) {
    szx *= scale;
    szy *= scale;
    let pos = Vec2_cast(position);
    let T = make_rotate_transform(angle);
    
    const dx = [1, 1, -1, -1], dy = [1, -1, -1, 1];
    ctx.beginPath();
    for (let i = 0; i < 4; i += 1) {
        let [x, y] = T.applyTo(new Vec2(szx * dx[i], szy * dy[i])).destruct();
        if (i == 0) {
            ctx.moveTo(x, y);
        } else {
            ctx.lineTo(x, y);
        }
    }
    ctx.fill();
}

function clear_board(ctx) {
    ctx.clearRect(0, 0, bwidth, bheight);
}

function Player(pos) {
    let collider_desc = Rapier.ColliderDesc.cuboid(this.width / 2, this.height / 2).setRestitution(0.5);
    let body_desc = Rapier.RigidBodyDesc.newDynamic().lockRotations().setTranslation(pos.x, pos.y).setLinearDamping(0.2);
    this.entity = world.createRigidBody(body_desc);
    world.createCollider(collider_desc, this.entity.handle);

    this.direction = 'left';
    // this.down_ttl = 0;
}

Player.prototype.height = 1.86;
Player.prototype.width = 0.7;
Player.prototype.speed = 15;

Player.prototype.update = function() {
    if (this.down_ttl > 0) {
        this.down_ttl -= 1;
    }
};

Player.prototype.goDown = function() {
    this.down_ttl = 15;
    this.entity.applyForce((new Vec2(0, 1000)).destruct2());
    // this.entity.wakeUp();
    this.entity.setTranslation(Vec2_cast(this.entity.translation()).add(new Vec2(0, -0.01)));
    this.entity.wakeUp();
};

Player.prototype.jump = function() {
    // this.entity.applyForce({ x: 0, y: -2000 });
    this.entity.setLinvel((new Vec2(this.entity.linvel().x, -20)).destruct2());
    this.entity.wakeUp();
};

Player.prototype.goLeft = function() {
    this.entity.setLinvel((new Vec2(-this.speed, this.entity.linvel().y)).destruct2());
    this.entity.wakeUp();
    this.direction = 'left';
}

Player.prototype.goRight = function() {
    this.entity.setLinvel((new Vec2(this.speed, this.entity.linvel().y)).destruct2());
    this.entity.wakeUp();
    this.direction = 'right';   
}

Player.prototype.draw = function(ctx) {
    let position = this.entity.translation();
    ctx.fillRect(scale * (position.x - this.width / 2), scale * (position.y - this.height / 2), scale * this.width, scale * this.height);

    if (this.direction == 'left') {
        ctx.fillRect(scale * (position.x - this.width / 2) - scale * 0.4, scale * (position.y - this.height / 2), scale * 0.4, scale * 0.4);
    } else if (this.direction == 'right') {
        ctx.fillRect(scale * (position.x + this.width / 2), scale * (position.y - this.height / 2), scale * 0.4, scale * 0.4);
    }
};

function StaticLand(pos, len) {
    this.len = len;

    let collider_desc = Rapier.ColliderDesc.cuboid(this.len / 2, this.thickness / 2).setRestitution(0.2).setFriction(0.2).setActiveHooks(Rapier.ActiveHooks.FILTER_CONTACT_PAIRS);
    let body_desc = Rapier.RigidBodyDesc.newStatic().setTranslation(pos.x, pos.y);
    this.entity = world.createRigidBody(body_desc);
    let collider = world.createCollider(collider_desc, this.entity.handle);
    land_handles.push(collider.handle);
}

StaticLand.prototype.thickness = 0.1;

StaticLand.prototype.update = function() {}

StaticLand.prototype.draw = function(ctx) {
    let position = this.entity.translation();
    ctx.fillRect(scale * (position.x - this.len / 2), scale * (position.y - this.thickness / 2), scale * this.len, scale * this.thickness);
}

function XMoveLand(pos, len, move_len, speed) {
    this.len = len;
    this.xleft = pos.x;
    this.xright = pos.x + move_len;
    this.speed = speed;
    this.direction = 'right';

    let collider_desc = Rapier.ColliderDesc.cuboid(this.len / 2, this.thickness / 2).setFriction(0.4).setActiveHooks(Rapier.ActiveHooks.FILTER_CONTACT_PAIRS);
    let body_desc = Rapier.RigidBodyDesc.newKinematicVelocityBased().setTranslation(pos.x, pos.y).lockRotations();
    this.entity = world.createRigidBody(body_desc);
    let collider = world.createCollider(collider_desc, this.entity.handle);
    this.entity.setLinvel((new Vec2(this.speed, 0)).destruct2());
    this.entity.wakeUp();
    land_handles.push(collider.handle);
}

XMoveLand.prototype.thickness = 0.1;

XMoveLand.prototype.update = function() {
    let position = this.entity.translation();
    if (position.x < this.xright && this.direction == 'right') {
        this.entity.setLinvel((new Vec2(this.speed, 0)).destruct2());
        this.entity.wakeUp();
    }

    if (position.x > this.xleft && this.direction == 'left') {
        this.entity.setLinvel((new Vec2(-this.speed, 0)).destruct2());
        this.entity.wakeUp();
    }

    if (position.x >= this.xright && this.direction == 'right') {
        this.direction = 'left';
    }

    if (position.x <= this.xleft && this.direction == 'left') {
        this.direction = 'right';
    }
}

XMoveLand.prototype.draw = function(ctx) {
    let position = this.entity.translation();
    ctx.fillRect(scale * (position.x - this.len / 2), scale * (position.y - this.thickness / 2), scale * this.len, scale * this.thickness);
}

function FilterHook() {}
FilterHook.prototype.filterContactPair = contact_handler;
FilterHook.prototype.filterIntersectionPair = function() {};

function contact_handler(c1, c2, b1, b2) {
    if (land_handles.includes(c1) || land_handles.includes(c2)) {
        if (!land_handles.includes(c1)) {
            // swap c1,c2 and b1,b2
            let tmp = c2;
            c2 = c1;
            c1 = tmp;
            tmp = b2;
            b2 = b1;
            b1 = tmp;
        }

        if (yMap[b2] > yMap[b1]) {
            return null;
        }

        if (b2 == playerA.entity.handle && playerA.down_ttl > 0) {
            return null;
        }

        return Rapier.SolverFlags.COMPUTE_IMPULSE;
    }
    return Rapier.SolverFlags.COMPUTE_IMPULSE;
}

function update() {
    yMap = [];
    world.forEachRigidBody(b => {
        let position = b.translation();
        yMap[b.handle] = position.y;
    });

    let event_queue = new Rapier.EventQueue(true);
    world.step(event_queue, new FilterHook());
    event_queue.free();

    playerA.update();
    for (const e of lands) {
        e.update();
    }

    clear_board(ctx);
    playerA.draw(ctx);
    for (const e of lands) {
        e.draw(ctx);
    }

    requestAnimationFrame(update);
}

function main() {
    ctx = document.getElementById('board').getContext('2d');

    world = new Rapier.World(gravity.destruct2());
    window.world = world;

    playerA = new Player(new Vec2(10, 10));
    document.addEventListener('keydown', function(e) {
        // console.log(e.code);
        if (e.code == 'ArrowUp') {
            playerA.jump();
        } else if (e.code == 'ArrowLeft') {
            playerA.goLeft();
        } else if (e.code == 'ArrowRight') {
            playerA.goRight();
        } else if (e.code == 'ArrowDown') {
            playerA.goDown();
        }
    });

    lands.push(new StaticLand(new Vec2(45, 50), 85));
    lands.push(new XMoveLand(new Vec2(20, 30), 30, 40, 4));

    update();
}

import('@dimforge/rapier2d').then(R => {
    Rapier = R;
    window.Rapier = R;
    main();
});